package com.project.common.data.mybatis.interceptor;

import com.project.common.data.mybatis.metadata.TableMetadata;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.builder.ResultMapResolver;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultFlag;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeAliasRegistry;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.apache.ibatis.type.UnknownTypeHandler;

import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.stream.Stream;

@Intercepts({
    @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class ResultMapInterceptor implements Interceptor {
    private static final Set<String> MAPPED_STATEMENT_CACHE = new ConcurrentSkipListSet<>();
    private static final TypeAliasRegistry ALIAS_REGISTRY = new TypeAliasRegistry();
    private static final String FIELD_NAME_MAPPED_STATEMENT = "mappedStatement";
    private static final String FIELD_NAME_CONFIGURATION = "configuration";
    private static final String FIELD_NAME_RESULT_MAP = "resultMaps";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        ResultSetHandler handler = (ResultSetHandler) invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(handler);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue(FIELD_NAME_MAPPED_STATEMENT);
        //只处理SELECT和未处理的map
        if (mappedStatement.getSqlCommandType() != SqlCommandType.SELECT
            || MAPPED_STATEMENT_CACHE.contains(mappedStatement.getId())) {
            return invocation.proceed();
        }
        String resource = mappedStatement.getResource();
        Configuration configuration = (Configuration) metaObject.getValue(FIELD_NAME_CONFIGURATION);
        String nameSpace = mappedStatement.getId().substring(0, mappedStatement.getId().lastIndexOf("."));
        MapperBuilderAssistant builderAssistant = new MapperBuilderAssistant(configuration, resource);
        builderAssistant.setCurrentNamespace(nameSpace);

        List<ResultMap> resultMaps = new ArrayList<>();
        for (ResultMap resultMap : mappedStatement.getResultMaps()) {
            if (!hasMappings(resultMap)) {
                resultMaps.add(resultMap);
                continue;
            }

            //PersistenceMapper.get/PersistenceMapper.listAll这里会生成相同的MAP
            String mapId = this.getResultMapId(TableMetadata.forClass(resultMap.getType())
                    .getEntityClass());
            if (!configuration.getResultMapNames().contains(mapId)) {
                resultMaps.add(this.buildResultMap(
                        TableMetadata.forClass(resultMap.getType()),
                        builderAssistant));
            } else {
                resultMaps.add(configuration.getResultMap(mapId));
            }
        }
        Field field = mappedStatement.getClass().getDeclaredField(FIELD_NAME_RESULT_MAP);
        field.setAccessible(true);
        field.set(mappedStatement, resultMaps);
        field.setAccessible(false);
        MAPPED_STATEMENT_CACHE.add(mappedStatement.getId());
        return invocation.proceed();
    }

    private boolean hasMappings(ResultMap resultMap) {
        Collection<Class<?>> simpleTypes = ALIAS_REGISTRY.getTypeAliases().values();
        if ((resultMap.getAutoMapping() != null && resultMap.getAutoMapping()) ||
            (simpleTypes.contains(resultMap.getType()))) {
            return false;
        }
        return (resultMap.getResultMappings() == null || resultMap.getResultMappings().isEmpty());
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    private ResultMap buildResultMap(TableMetadata tableMetadata, MapperBuilderAssistant builderAssistant) {
        List<ResultMapping> resultMappings = new ArrayList<>();
        Stream.of(tableMetadata.getColumns()).forEach(tableColumn -> {
            List<ResultFlag> flags = new ArrayList<>();
            if (tableColumn.isKey()) {
                flags.add(ResultFlag.ID);
            }
            ResultMapping.Builder builder = new ResultMapping.Builder(
                builderAssistant.getConfiguration(),
                tableColumn.getProperty(),
                tableColumn.getName(),
                tableColumn.getType()).flags(flags)
                .jdbcType(tableColumn.getJdbcType());
            if (tableColumn.getTypeHandler() != null) {
                builder.typeHandler(
                    resolveTypeHandler(tableColumn.getType(), tableColumn.getTypeHandler(), builderAssistant));
            }
            resultMappings.add(builder.build());
        });
        Class<?> entityClass = tableMetadata.getEntityClass();
        ResultMapResolver resultMapResolver = new ResultMapResolver(
            builderAssistant, this.getResultMapId(entityClass), entityClass,
            null, null, resultMappings, null);
        return resultMapResolver.resolve();
    }

    private String getResultMapId(Class<?> clazz) {
        return clazz.getSimpleName() +
                "Map";
    }

    protected TypeHandler<?> resolveTypeHandler(Class<?> javaType,
                                                Class<? extends TypeHandler<?>> typeHandlerType,
                                                MapperBuilderAssistant builderAssistant) {
        if (typeHandlerType == null || typeHandlerType == UnknownTypeHandler.class) {
            return null;
        }
        TypeHandlerRegistry typeHandlerRegistry = builderAssistant.getConfiguration().getTypeHandlerRegistry();
        TypeHandler<?> handler = typeHandlerRegistry.getMappingTypeHandler(typeHandlerType);
        if (handler == null) {
            handler = typeHandlerRegistry.getInstance(javaType, typeHandlerType);
        }
        return handler;
    }
}
